/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.bus.runner.adapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.bus.runner.config.MessageBusProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.interceptor.WireTap;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.xd.dirt.integration.bus.MessageBus;
import org.springframework.xd.dirt.integration.bus.XdHeaders;
/**
* @author Mark Fisher
* @author Dave Syer
*/
@ManagedResource
public class MessageBusAdapter implements Lifecycle, ApplicationContextAware {
private static Logger logger = LoggerFactory.getLogger(MessageBusAdapter.class);
private MessageBus messageBus;
private MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
private Collection<OutputChannelBinding> outputChannels = Collections.emptySet();
private Collection<InputChannelBinding> inputChannels = Collections.emptySet();
private boolean running = false;
private final AtomicBoolean active = new AtomicBoolean(false);
private boolean trackHistory = false;
private MessageBusProperties module;
private ConfigurableApplicationContext applicationContext;
private ChannelLocator inputChannelLocator;
private ChannelLocator outputChannelLocator;
private DestinationResolver<MessageChannel> channelResolver;
private Map<String, String> bindings = new HashMap<String, String>();
public MessageBusAdapter(MessageBusProperties module, MessageBus messageBus) {
this.module = module;
this.messageBus = messageBus;
this.inputChannelLocator = new DefaultChannelLocator(module);
this.outputChannelLocator = new DefaultChannelLocator(module);
}
public void setInputChannelLocator(ChannelLocator channelLocator) {
this.inputChannelLocator = channelLocator;
}
public void setOutputChannelLocator(ChannelLocator channelLocator) {
this.outputChannelLocator = channelLocator;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
this.channelResolver = new BeanFactoryChannelResolver(applicationContext);
}
public void setChannelResolver(DestinationResolver<MessageChannel> channelResolver) {
this.channelResolver = channelResolver;
}
public void setMessageBuilderFactory(MessageBuilderFactory messageBuilderFactory) {
this.messageBuilderFactory = messageBuilderFactory;
}
public void setTrackHistory(boolean trackHistory) {
this.trackHistory = trackHistory;
}
public void setOutputChannels(Collection<OutputChannelBinding> outputChannels) {
this.outputChannels = new LinkedHashSet<OutputChannelBinding>(outputChannels);
}
public void setInputChannels(Collection<InputChannelBinding> inputChannels) {
this.inputChannels = new LinkedHashSet<InputChannelBinding>(inputChannels);
}
public ChannelsMetadata getChannelsMetadata() {
ChannelsMetadata channels = new ChannelsMetadata();
channels.setModule(this.module);
channels.setInputChannels(new LinkedHashSet<InputChannelBinding>(this.inputChannels));
channels.setOutputChannels(new LinkedHashSet<OutputChannelBinding>(this.outputChannels));
return channels;
}
public OutputChannelBinding getOutputChannel(String name) {
if (name == null) {
return null;
}
for (OutputChannelBinding binding : this.outputChannels) {
if (name.equals(binding.getRemoteName())) {
return binding;
}
}
for (OutputChannelBinding binding : this.outputChannels) {
if (name.equals(binding.getLocalName())) {
return binding;
}
}
return null;
}
public InputChannelBinding getInputChannel(String name) {
if (name == null) {
return null;
}
for (InputChannelBinding binding : this.inputChannels) {
if (name.equals(binding.getRemoteName())) {
return binding;
}
}
for (InputChannelBinding binding : this.inputChannels) {
if (name.equals(binding.getLocalName())) {
return binding;
}
}
return null;
}
public void tap(String outputChannel) {
OutputChannelBinding channel = getOutputChannel(outputChannel);
if (channel == null || channel.isTapped()) {
return;
}
createAndBindTapChannel(channel.getTapChannelName(), channel.getLocalName());
channel.setTapped(true);
}
public void untap(String outputChannel) {
OutputChannelBinding channel = getOutputChannel(outputChannel);
if (channel == null || !channel.isTapped()) {
return;
}
String tapChannelName = channel.getTapChannelName();
this.messageBus.unbindProducers(tapChannelName);
channel.setTapped(false);
}
@ManagedOperation
public void rebind() {
boolean runnable = locateChannels();
if (runnable && !this.running) {
start();
}
if (!runnable && this.running) {
stop();
}
}
@Override
@ManagedOperation
public void start() {
if (!this.running) {
// Start everything, but don't call ourselves
if (!this.active.get()) {
if (this.active.compareAndSet(false, true)) {
boolean ready = bindChannels();
if (ready) {
this.running = true;
this.applicationContext.start();
}
this.active.set(false);
}
}
}
}
@Override
@ManagedOperation
public void stop() {
if (this.running) {
if (!this.active.get()) {
if (this.active.compareAndSet(false, true)) {
unbindChannels();
this.applicationContext.stop();
this.active.set(false);
}
}
}
this.running = false;
}
@Override
@ManagedAttribute
public boolean isRunning() {
return this.running && this.applicationContext.isRunning();
}
protected final void unbindChannels() {
for (InputChannelBinding binding : this.inputChannels) {
String name = this.bindings.get(binding.getRemoteName());
if (name == null) {
continue;
}
this.messageBus.unbindConsumers(name);
}
for (OutputChannelBinding binding : this.outputChannels) {
String name = this.bindings.get(binding.getRemoteName());
if (name == null) {
continue;
}
this.messageBus.unbindProducers(name);
if (binding.isTapped()) {
String tapChannelName = binding.getTapChannelName();
this.messageBus.unbindProducers(tapChannelName);
}
}
}
protected final boolean bindChannels() {
if (!locateChannels()) {
return false;
}
Map<String, Object> historyProperties = new LinkedHashMap<String, Object>();
if (this.trackHistory) {
// TODO: addHistoryTag();
}
for (OutputChannelBinding binding : this.outputChannels) {
String name = binding.getRemoteName();
MessageChannel outputChannel = this.channelResolver.resolveDestination(binding.getLocalName());
bindMessageProducer(outputChannel, name, this.module.getProducerProperties());
if (binding.isTapped()) {
String tapChannelName = getTapChannelName(name);
binding.setTapChannelName(tapChannelName);
// tappableChannels.put(tapChannelName, outputChannel);
// if (isTapActive(tapChannelName)) {
createAndBindTapChannel(tapChannelName, name);
// }
}
if (this.trackHistory) {
historyProperties.put("outputChannel", name);
track(outputChannel, historyProperties);
}
}
for (InputChannelBinding binding : this.inputChannels) {
String name = binding.getRemoteName();
MessageChannel inputChannel = this.channelResolver.resolveDestination(binding.getLocalName());
bindMessageConsumer(inputChannel, name, this.module.getConsumerProperties());
if (this.trackHistory && this.outputChannels.size() != 1) {
historyProperties.put("inputChannel", name);
track(inputChannel, historyProperties);
}
}
return true;
}
private boolean locateChannels() {
logger.info("Locating channels");
boolean located = true;
for (OutputChannelBinding binding : this.outputChannels) {
String name = this.outputChannelLocator.locate(binding.getLocalName());
if (name == null) {
logger.info("No channel found for: " + binding.getLocalName());
located = false;
}
binding.setRemoteName(name);
this.bindings.put(binding.getRemoteName(), name);
}
for (InputChannelBinding binding : this.inputChannels) {
String name = this.inputChannelLocator.locate(binding.getLocalName());
if (name == null) {
logger.info("No channel found for: " + binding.getLocalName());
located = false;
}
binding.setRemoteName(name);
this.bindings.put(binding.getRemoteName(), name);
}
return located;
}
// TODO: move this to ChannelLocator?
private String getTapChannelName(String name) {
return !isDefaultOuputChannel(name) ? this.module.getTapChannelName(getPlainChannelName(name))
: this.module.getTapChannelName();
}
// TODO: move this to ChannelLocator?
private String getPlainChannelName(String name) {
if (name.contains(":")) {
name = name.substring(name.indexOf(":") + 1);
}
return name;
}
// TODO: move this to ChannelLocator?
private boolean isDefaultOuputChannel(String channelName) {
if (channelName.contains(":")) {
String[] tokens = channelName.split(":", 2);
channelName = tokens[1];
}
return channelName.equals(this.module.getOutputChannelName());
}
/*
* Following methods copied from parent to support the bindChannels() method above
*/
private void bindMessageConsumer(MessageChannel inputChannel,
String inputChannelName, Properties consumerProperties) {
if (isChannelPubSub(inputChannelName)) {
this.messageBus.bindPubSubConsumer(inputChannelName, inputChannel, consumerProperties);
}
else {
this.messageBus.bindConsumer(inputChannelName, inputChannel, consumerProperties);
}
}
private void bindMessageProducer(MessageChannel outputChannel,
String outputChannelName, Properties producerProperties) {
if (isChannelPubSub(outputChannelName)) {
this.messageBus.bindPubSubProducer(outputChannelName, outputChannel, producerProperties);
}
else {
this.messageBus.bindProducer(outputChannelName, outputChannel, producerProperties);
}
}
private boolean isChannelPubSub(String channelName) {
Assert.isTrue(StringUtils.hasText(channelName), "Channel name should not be empty/null.");
return (channelName.startsWith("tap:") || channelName.startsWith("topic:"));
}
/**
* Creates a wiretap on the output channel and binds the tap channel to
* {@link MessageBus}'s message target.
*
* @param tapChannelName the name of the tap channel
* @param localName the channel to tap
*/
private void createAndBindTapChannel(String tapChannelName, String localName) {
logger.info("creating and binding tap channel for {}", tapChannelName);
MessageChannel channel = this.channelResolver.resolveDestination(localName);
if (channel instanceof ChannelInterceptorAware) {
DirectChannel tapChannel = new DirectChannel();
tapChannel.setBeanName(tapChannelName + ".tap.bridge");
this.messageBus.bindPubSubProducer(tapChannelName, tapChannel, null); // TODO
// tap
// producer
// props
tapOutputChannel(tapChannel, (ChannelInterceptorAware) channel);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("output channel is not interceptor aware. Tap will not be created.");
}
}
}
private MessageChannel tapOutputChannel(MessageChannel tapChannel, ChannelInterceptorAware outputChannel) {
outputChannel.addInterceptor(new WireTap(tapChannel));
return tapChannel;
}
private void track(MessageChannel channel, final Map<String, Object> historyProps) {
if (channel instanceof ChannelInterceptorAware) {
((ChannelInterceptorAware) channel)
.addInterceptor(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
@SuppressWarnings("unchecked")
Collection<Map<String, Object>> history = (Collection<Map<String, Object>>) message
.getHeaders().get(XdHeaders.XD_HISTORY);
if (history == null) {
history = new ArrayList<Map<String, Object>>(1);
}
else {
history = new ArrayList<Map<String, Object>>(history);
}
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.putAll(historyProps);
map.put("thread", Thread.currentThread().getName());
history.add(map);
Message<?> out = MessageBusAdapter.this.messageBuilderFactory.fromMessage(message)
.setHeader(XdHeaders.XD_HISTORY, history).build();
return out;
}
});
}
}
}